# -*- coding: utf-8 -*-

import ast

from ir.ir_helper import Helper, ClassHelper
from ir.ir_ports import ITypePortIn, ITypePortOut, ITypeLane, ITypeLanePortSender, ITypeLanePortReceiver
from ir.ir_ds import ITypeStruct

from .convert_type import convert_ast_to_type
from .helper_utils import create_class_helper


def get_lane_port_info(ast_ports: ast.List, helper: Helper):
    # Prepare the port list.
    ports = []
    # Loop for all the elements in the port definitions.
    for ast_port_def in ast_ports.elts:
        port_name = ''
        port_size = -1
        port_type = None
        # Check the port definition type.
        # Check the type of ast port definition.
        if isinstance(ast_port_def, ast.Str):
            # Cannot use string only to define the port.
            raise Exception('Please define the port {} in tuple or dict.'.format(ast_port_def.s))
        elif isinstance(ast_port_def, ast.Tuple):
            ast_tuple_elts = ast_port_def.elts
            # Check whether the size is valid.
            if len(ast_tuple_elts) != 3:
                raise Exception('Please use format (name, size, type) to define the port in tuple, you have {} values: {}'.format(
                    len(ast_tuple_elts), ast_tuple_elts))
            # The first item must be the name of the port.
            if not isinstance(ast_tuple_elts[0], ast.Str):
                raise Exception('To define port using tuple, the first element must be name.')
            port_name = ast_tuple_elts[0].s
            # Check whether the size is valid.
            # For -1, Python is fucking us using a unary expression.
            if isinstance(ast_tuple_elts[1], ast.UnaryOp) and \
                    isinstance(ast_tuple_elts[1].op, ast.USub) and \
                    isinstance(ast_tuple_elts[1].operand, ast.Num):
                # A minus number is detected.
                port_size = -1 * ast_tuple_elts[1].operand.n
            elif isinstance(ast_tuple_elts[1], ast.Num) and \
                    isinstance(ast_tuple_elts[1].n, int):
                # Directly fetch the value from Num.
                port_size = ast_tuple_elts[1].n
            else:
                raise Exception('To define port using tuple, the second element must be the maximum size in integer.')

            # Check whether the third item is a type.
            if not isinstance(ast_tuple_elts[2], ast.Name):
                raise Exception('To define port using tuple, the third element must be the port management type.')
            port_type = ast_tuple_elts[2].id
        elif isinstance(ast_port_def, ast.Dict):
            # Loop for keys and values.
            for ast_key, ast_value in zip(ast_port_def.keys,
                                          ast_port_def.values):
                # Check the key.
                if not isinstance(ast_key, ast.Str):
                    raise Exception('To define port using dict, the key must be string.')
                key_name = ast_key.s
                if key_name == 'name':
                    # Check name value.
                    if not isinstance(ast_value, ast.Str):
                        raise Exception('To define port using dict, the port name must be string.')
                    port_name = ast_value.s
                elif key_name == 'max_size':
                    # Check max size value.
                    if not isinstance(ast_value, ast.Num) or not isinstance(ast_value.n, int):
                        raise Exception('To define port using dict, the maximum port size must be int.')
                    port_size = ast_value.n
                elif key_name == 'type':
                    # Check the type of the value.
                    if not isinstance(ast_value, ast.Name):
                        raise Exception('To define port using dict, the type port must be a valid type name.')
                    port_type = ast_value.id
        else:
            raise Exception('Unexpected format for Lane port definition.')
        # Check the port type.
        if port_type is None:
            raise Exception('Please specify the port management type.')
        # Convert port type into IType.
        managed_type = convert_ast_to_type(port_type, helper)
        if managed_type is None:
            raise Exception('Failed to convert managed type {}'.format(port_type))
        ports.append([port_name, port_size, managed_type])
    return ports


def get_lane_port_order(ast_get_format: ast.List):
    # The port definition should be a list.
    if not isinstance(ast_get_format, ast.List):
        raise Exception('All Lane port order definitions should be a list.')
    port_name_order = []
    # Loop and parse the order.
    for ast_port_name in ast_get_format.elts:
        # It must be string.
        if not isinstance(ast_port_name, ast.Str):
            raise Exception('Lane get format must provide port name as string.')
        # Get the port name.
        port_name_order.append(ast_port_name.s)
    return port_name_order


def generate_lane_type(lane_name: str, ast_ports, ast_get_format, helper: Helper):
    # Generate the port and get format definition.
    port_info = get_lane_port_info(ast_ports, helper)
    port_order = get_lane_port_order(ast_get_format)
    # Based on the port information and order, generate the Sender and Receiver
    # class definition.
    sender_type = ITypeLanePortSender(lane_name)
    for port_name, _, managed_type in port_info:
        sender_type.ports[port_name] = ITypePortOut(managed_type)
    receiver_type = ITypeLanePortReceiver(lane_name)
    receiver_type.order = port_order
    for port_name, max_size, managed_type in port_info:
        receiver_type.ports[port_name] = ITypePortIn(managed_type, max_size)
    return sender_type, receiver_type


def create_lane_sender_helper(sender_type: ITypeLanePortSender, helper: Helper):
    # Generate a helper from parent type.
    port_helper = create_class_helper('LanePortSender', helper.chelper)
    # Need to modify the put method.
    params = []
    for member in sender_type.ports:
        # Append the member to params.
        params.append([sender_type.ports[member].managed_type, member, None])
    # Update the put definition.
    put_define = port_helper.func['put'].copy()
    put_define[1] = params
    port_helper.func['put'] = put_define
    # Set the member data.
    port_helper.member = sender_type.ports
    # Give back the port helper.
    return port_helper


def create_lane_receiver_helper(receiver_type: ITypeLanePortReceiver, pack_type, helper: Helper):
    # Generate a helper from parent type.
    port_helper = create_class_helper('LanePortReceiver', helper.chelper)
    # Need to modify the get method.
    get_define = port_helper.func['get'].copy()
    get_define[0] = pack_type
    port_helper.func['get'] = get_define
    # Set the member data.
    port_helper.member = receiver_type.ports
    # Give back the port helper.
    return port_helper


def generate_lane_pack_type(lane_name: str, receiver_type):
    # Create the lane pack structure.
    return ITypeStruct(lane_name + 'Pack',
                       receiver_type.order,
                       list(receiver_type.ports[x].managed_type for x in receiver_type.order),
                       extract=True)


def create_packge_helper(pack_type: ITypeStruct):
    # Create the helper.
    pack_helper = ClassHelper()
    pack_helper.base = 'struct'
    # Only focus on members.
    for member_name, member_type in zip(pack_type.member, pack_type.member_type):
        pack_helper.member[member_name] = member_type
    return pack_helper


def convert_lane(ast_lane: ast.ClassDef, helper: Helper):
    # Get the name of Lane.
    lane_name = ast_lane.name
    # Extract the port definition and format defintion.
    ast_ports = None
    ast_get_format = None
    for ast_line in ast_lane.body:
        if not isinstance(ast_line, ast.Assign):
            continue
        # Check the target name of the line.
        if len(ast_line.targets) != 1 or not isinstance(ast_line.targets[0], ast.Name):
            continue
        # Ignore the value which is not list.
        if not isinstance(ast_line.value, ast.List):
            continue
        # Check the target name.
        target_name = ast_line.targets[0].id
        if target_name == 'ports':
            ast_ports = ast_line.value
        elif target_name == 'get_format':
            ast_get_format = ast_line.value
    # Check port and get format definition.
    if ast_ports is None or ast_get_format is None:
        raise Exception('Lane {} should have both port and get format assignment'.format(lane_name))
    # Convert sender and receivers.
    sender_type, receiver_type = generate_lane_type(lane_name, ast_ports, ast_get_format, helper)
    # Create the lane pack structure.
    pack_type = generate_lane_pack_type(lane_name, receiver_type)
    # Create the helper for the sender and receiver type.
    sender_helper = create_lane_sender_helper(sender_type, helper)
    receiver_helper = create_lane_receiver_helper(receiver_type, pack_type, helper)
    # Construct the lane type.
    lane_type = ITypeLane(lane_name)
    lane_helper = create_class_helper('Lane', helper.chelper)
    pack_helper = create_packge_helper(pack_type)
    return [sender_type, receiver_type, lane_type, pack_type], \
           [sender_helper, receiver_helper, lane_helper, pack_helper]
